package com.chuxing.security.service.impl;

import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chuxing.domain.Contest;
import com.chuxing.domain.DTO.*;
import com.chuxing.domain.User;
import com.chuxing.domain.VO.GetUserInfoVO;
import com.chuxing.domain.VO.PageResultVO;
import com.chuxing.exception.GenericException;
import com.chuxing.exception.UnauthorizedException;
import com.chuxing.security.mapper.ContestMapper;
import com.chuxing.security.mapper.UserMapper;
import com.chuxing.security.service.ImageService;
import com.chuxing.security.service.UserService;
import com.chuxing.utils.RedisCache;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

/**
 * @author HP
 * &#064;description  针对表【t_user(用户信息表)】的数据库操作Service实现
 * &#064;createDate  2024-11-16 15:24:49
 */
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {
    // 注入UserMapper
    private final UserMapper userMapper;
    // 注入Redis缓存工具类
    private final RedisCache redisCache;
    // 注入ContestMapper
    private final ContestMapper contestMapper;
    private final ImageService imageService;

    /**
     * 根据用户名选择用户
     *
     * @param s 字符串参数，代表要查询的用户名
     * @return 返回一个User对象，表示查询到的用户信息如果没有找到对应的用户信息，则返回null
     */
    @Override
    public User selectByUsername(String s) {
        return userMapper.selectByStudentId(s);
    }

    /**
     * 用户登录方法
     *
     * @param loginDTO 包含用户名和密码的登录数据传输对象
     * @throws GenericException 如果用户名或密码为空，或者验证失败，则抛出此异常
     */
    @Override
    public void login(LoginDTO loginDTO) {
        // 从登录DTO中获取用户名和密码
        String username = loginDTO.getUsername();
        String password = loginDTO.getPassword();

        // 检查用户名和密码是否为空
        if (username == null || password == null) {
            throw new GenericException("用户名或密码错误");
        }
        // 通过用户名查询用户信息
        User user = selectByUsername(username);
        // 检查用户是否存在
        if (user == null) {
            throw new GenericException("用户名或密码错误");
        }
        // 使用BCrypt算法验证密码
        if (!BCrypt.checkpw(password, user.getPassword())) {
            throw new GenericException("用户名或密码错误");
        }
        // 使用Sa-Token框架的StpUtil完成用户登录
        StpUtil.login(username);
    }

    /**
     * 退出登录功能
     *
     * @throws GenericException 如果用户未登录时调用此方法，将抛出一个通用异常，提示"未登录"
     */
    @Override
    public void logout() {
        //检查账号是否已经登录
        if (!StpUtil.isLogin()) {
            // 如果用户未登录，抛出异常
            throw new UnauthorizedException("未登录");
        }
        // 执行退出登录操作
        StpUtil.logout();
    }

    /**
     * 注册用户
     *
     * @param registerDTO 包含用户注册信息的数据传输对象
     * @throws GenericException 如果验证码不正确、用户名已存在或验证码未发送
     */
    @Override
    public void register(RegisterDTO registerDTO) {
        // 获取用户注册信息
        String username = registerDTO.getUsername();
        String password = registerDTO.getPassword();
        String confirmPassword = registerDTO.getConfirmPassword();
        String email = registerDTO.getEmail();
        String code = registerDTO.getCode();

        // 两次密码不一致，抛出异常
        if (!password.equals(confirmPassword)){
            throw new GenericException("两次密码不一致");
        }

        // 从缓存中获取邮箱验证码
        String emailCode = redisCache.getCacheObject("email" + email);

        // 验证码不存在，抛出异常
        if (emailCode == null) {
            throw new GenericException("请先发送验证码");
        }
        // 验证码不匹配，抛出异常
        if (!emailCode.equals(code)) {
            throw new GenericException("验证码错误");
        }
        // 验证码正确，清除缓存
        redisCache.deleteObject("email" + email);
        // 检查用户名是否已存在
        User user = selectByUsername(username);
        if (user != null) {
            throw new GenericException("用户名已存在");
        }

        // 检查邮箱是否已注册
        if (userMapper.selectByEmail(email) != null) {
            throw new GenericException("邮箱已存在");
        }

        // 创建新用户
        User newUser = new User();
        newUser.setStudentId(username);
        // 密码加密存储
        newUser.setPassword(BCrypt.hashpw(password, BCrypt.gensalt()));
        newUser.setEmail(email);
        newUser.setSign(0);
        // 设置创建时间和更新时间
        newUser.setCreateTime(LocalDateTime.now());
        newUser.setUpdateTime(LocalDateTime.now());

        // 插入用户到数据库
        userMapper.insert(newUser);
    }

    /**
     * 获取当前用户的角色列表
     *
     * @return 角色列表如果用户未登录，则抛出异常
     * @throws GenericException 如果用户未登录，抛出此异常
     */
    @Override
    public List<String> getListOfRoles() {
        // 检查用户是否已登录
        if (!StpUtil.isLogin()) {
            // 如果用户未登录，抛出异常
            throw new UnauthorizedException("未登录");
        }
        // 返回当前用户的角色列表
        return StpUtil.getRoleList();
    }

    /**
     * 重置密码方法
     *
     * @param resetPasswordDTO 包含用户邮箱、新密码和验证码的DTO对象
     * @throws GenericException 如果验证码验证失败或用户不存在，则抛出此异常
     */
    @Override
    public void resetPassword(ResetPasswordDTO resetPasswordDTO) {
        // 获取用户邮箱、新密码和验证码等
        String username = resetPasswordDTO.getUsername();
        String email = resetPasswordDTO.getEmail();
        String newPassword = resetPasswordDTO.getPassword();
        String newPasswordConfirm = resetPasswordDTO.getConfirmPassword();
        String code = resetPasswordDTO.getCode();
        // 两次密码不一致，抛出异常
        if (!newPassword.equals(newPasswordConfirm)){
            throw new GenericException("两次密码不一致");
        }
        // 从缓存中获取邮箱对应的验证码
        String emailCode = redisCache.getCacheObject("email" + email);
        // 验证码不存在，抛出异常
        if (emailCode == null) {
            throw new GenericException("请先发送验证码");
        }
        // 验证码不匹配，抛出异常
        if (!emailCode.equals(code)) {
            throw new GenericException("验证码错误");
        }
        // 根据邮箱查询用户信息
        User user = userMapper.selectByEmail(email);
        // 用户不存在，抛出异常
        if (user == null) {
            throw new GenericException("用户不存在");
        }
        // 检查用户名和邮箱是否匹配，不匹配则抛出异常
        if (!username.equals(user.getStudentId())){
            throw new GenericException("账号与邮箱不匹配");
        }
        // 更新用户密码并设置更新时间
        user.setPassword(BCrypt.hashpw(newPassword, BCrypt.gensalt()));
        user.setUpdateTime(LocalDateTime.now());
        // 更新数据库中的用户信息
        userMapper.updateByStudentId(user);
        // 清除缓存中的验证码
        redisCache.deleteObject("email" + email);
    }

//    /**
//     * 重置用户密码
//     * 该方法允许管理员重置任意用户的密码
//     *
//     * @param resetPasswordByAdminDTO 包含用户名和新密码的DTO
//     * @throws GenericException 如果当前用户未登录、权限不足、用户名或密码为空，或用户不存在时抛出异常
//     */
//    @Override
//    public void resetPasswordByAdmin(ResetPasswordByAdminDTO resetPasswordByAdminDTO) {
//        // 获取用户名和新密码
//        String username = resetPasswordByAdminDTO.getUsername();
//        String newPassword = resetPasswordByAdminDTO.getNewPassword();
//
//        // 检查当前用户是否已登录
//        if (!StpUtil.isLogin()) {
//            throw new UnauthorizedException("未登录");
//        }
//        // 检查当前用户是否具有管理员权限
//        if (!StpUtil.hasRole(SecurityRole.ROLE_ADMIN)) {
//            throw new GenericException("权限不足");
//        }
//        // 检查用户是否是管理员,不能重置管理员密码
//        if (StpUtil.hasRole(username, SecurityRole.ROLE_ADMIN) && !StpUtil.hasRole(SecurityRole.ROLE_SUPER_ADMIN)) {
//            throw new GenericException("权限不足");
//        }
//        // 验证用户名是否为空
//        if (username == null) {
//            throw new GenericException("用户名不能为空");
//        }
//        // 验证新密码是否为空
//        if (newPassword == null) {
//            throw new GenericException("新密码不能为空");
//        }
//        // 根据用户名查询用户
//        User user = selectByUsername(username);
//
//        // 检查用户是否存在
//        if (user == null) {
//            throw new GenericException("用户不存在");
//        }
//        // 更新用户密码和更新时间
//        user.setPassword(BCrypt.hashpw(newPassword, BCrypt.gensalt()));
//        user.setUpdateTime(LocalDateTime.now());
//
//        // 更新数据库中的用户信息
//        userMapper.updateByStudentId(user);
//    }

    /**
     * 获取用户信息
     *
     * @return GetUserInfoVO对象，包含用户信息和参赛记录
     * @throws UnauthorizedException 如果用户未登录
     * @throws GenericException      如果用户不存在
     */
    @Override
    @Cacheable(cacheNames = "userInfo", key = "#username")
    public GetUserInfoVO getUserInfo(String username) {
        log.info("查询数据库处理保存用户信息请求{}", username);
        // 通过用户名查询用户信息
        User user = selectByUsername(username);
        // 如果用户不存在，抛出异常
        if (user == null) {
            throw new GenericException("用户不存在");
        }
        // 查询用户的参赛记录
        List<Contest> contest = contestMapper.selectByStudentId(username);
        // 将用户信息和参赛记录封装到GetUserInfoVO对象中并返回
        GetUserInfoVO getUserInfoVO = new GetUserInfoVO();
        // 将用户信息复制到GetUserInfoVO对象中
        BeanUtil.copyProperties(user, getUserInfoVO);
        // 将参赛记录复制到GetUserInfoVO对象中
        getUserInfoVO.setContest(contest);
        //将密码信息去除
        getUserInfoVO.setPassword(null);
        return getUserInfoVO;
    }

    /**
     * 保存用户信息
     *
     * @param userInfoDTO 用户信息数据传输对象，包含需要保存或更新的用户信息
     */
    @Override
    @CacheEvict(value = "userInfo", key = "#userInfoDTO.studentId")
    public void saveUserInfo(UserInfoDTO userInfoDTO) {
        String username = StpUtil.getLoginIdAsString();
        if(!StpUtil.getLoginIdAsString().equals(username)){
            throw new GenericException("权限不足");
        }
        // 通过用户名查询用户信息
        User user = selectByUsername(username);
        // 如果用户不存在，抛出异常
        if (user == null) {
            throw new GenericException("用户不存在");
        }
        // 更新用户信息
        BeanUtils.copyProperties(userInfoDTO, user);
        user.setUpdateTime(LocalDateTime.now());
        // 更新用户信息到数据库
        userMapper.updateByStudentId(user);

        // 处理用户参与的竞赛信息
        List<Contest> contest = userInfoDTO.getContest();
        // 先删除用户所有的竞赛记录
        contestMapper.deleteByStudentId(username);
        // 再插入更新后的竞赛记录
        for (Contest contest1 : contest) {
            contest1.setStudentId(username);
            contestMapper.insert(contest1);
        }
    }

    /**
     * 根据管理员权限删除指定用户
     * 此方法首先检查传入的用户ID列表是否为空，然后验证当前操作用户是否已登录并具有管理员权限
     * 接着检查待删除的用户中是否有管理员，如果有且当前用户不是超级管理员，则不允许删除
     * 最后，调用Mapper方法删除用户，并为被删除用户执行登出操作
     *
     * @param ids 待删除用户的ID列表，不能为null或空
     * @throws UnauthorizedException 如果用户ID列表为空或当前用户未登录或权限不足
     * @throws GenericException      如果尝试删除管理员用户且当前用户不是超级管理员
     */
    @Override
    @CacheEvict(value = "userInfo", allEntries=true)
    public void deleteUserByAdmin(List<String> ids) {
        // 检查用户ID列表是否为空
        if (ids == null || ids.isEmpty()) {
            throw new UnauthorizedException("用户ID列表不能为空");
        }
        // 调用Mapper方法删除用户
        userMapper.deleteUserByAdmin(ids);
        // 为被删除用户执行登出操作
        for (String id : ids) {
            //删除对应的用户图片信息
            imageService.deleteImageByAdmin(id);
            // 删除对应的竞赛信息
            contestMapper.deleteByStudentId(id);
            StpUtil.logout(id);
        }
    }


    /**
     * 根据用户ID获取用户信息
     *
     * @param id 用户ID，用于查询用户信息和比赛信息
     * @return 返回包含用户信息和比赛信息的GetUserInfoVO对象
     * @throws GenericException 如果用户没有管理员权限或用户不存在，抛出此异常
     */
    @Override
    @Cacheable(cacheNames = "userInfo", key = "#id")
    public GetUserInfoVO getUserById(String id) {
        return getUserInfo(id);
    }

    /**
     * 获取用户列表
     * 此方法用于获取系统中的用户列表，仅限已登录且具有管理员角色的用户访问
     * 它实现了分页查询，以提高查询效率和性能
     *
     * @param getUserListDTO 包含分页信息和查询条件的DTO对象
     * @return 返回一个包含用户列表和分页信息的PageResultVO对象
     * @throws UnauthorizedException 如果用户未登录，则抛出此异常
     * @throws GenericException      如果用户权限不足或系统内部发生错误，则抛出此异常
     */
    @Override
    public PageResultVO getUserList(GetUserListDTO getUserListDTO) {
        // 验证分页参数
        int pageNum = Math.max(1, getUserListDTO.getPageNum());
        int pageSize = Math.max(1, getUserListDTO.getPageSize());
        // 启用分页查询
        PageHelper.startPage(pageNum, pageSize);
        try {
            try (Page<User> page = userMapper.selectUserList(getUserListDTO)) {
                //将密码清除
                page.getResult().forEach(user -> user.setPassword(null));
                // 构造并返回分页结果对象
                return new PageResultVO(page.getTotal(), page.getResult());
            }
        } catch (Exception e) {
            // 记录异常日志
            log.error("获取用户列表时发生错误", e);
            // 抛出通用异常，不暴露具体错误信息
            throw new GenericException("系统内部错误");
        } finally {
            // 确保分页查询结束
            PageHelper.clearPage();
        }
    }

    /**
     * 管理员添加用户方法
     * 此方法首先检查当前操作者是否已登录并具有管理员权限，以确保操作的安全性
     * 然后验证待添加用户的邮箱和学生ID是否已存在于系统中，以保证用户信息的唯一性
     * 最后，将用户信息从DTO对象转换并保存到数据库中，完成用户添加操作
     *
     * @param addUserByAdminDTO 包含待添加用户信息的数据传输对象
     * @throws UnauthorizedException 如果当前操作者未登录或权限不足，抛出此异常
     * @throws GenericException 如果用户邮箱或学生ID已存在，抛出此异常
     */
    @Override
    public void addUserByAdmin(AddUserByAdminDTO addUserByAdminDTO) {
        //检查输入信息是否为空
        if(StrUtil.isBlank(addUserByAdminDTO.getStudentId())){
            throw new GenericException("学生ID不能为空");
        }
        if(StrUtil.isBlank(addUserByAdminDTO.getEmail())){
            throw new GenericException("邮箱不能为空");
        }
        if(StrUtil.isBlank(addUserByAdminDTO.getPassword())){
            throw new GenericException("密码不能为空");
        }
        // 设置默认权限
        if(addUserByAdminDTO.getSign()==null){
            addUserByAdminDTO.setSign(0);
        }
        // 检查邮箱是否已注册
        if (userMapper.selectByEmail(addUserByAdminDTO.getEmail()) != null) {
            throw new GenericException("邮箱已存在");
        }
        // 检查学生ID是否已注册
        if (userMapper.selectByStudentId(addUserByAdminDTO.getStudentId()) != null) {
            throw new GenericException("账号已存在");
        }
        // 创建User对象并复制属性
        User user = new User();
        BeanUtil.copyProperties(addUserByAdminDTO, user);
        // 对用户密码进行加密处理
        user.setPassword(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt()));
        // 设置用户创建时间和更新时间为当前时间
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        // 将用户信息插入数据库
        userMapper.insert(user);
    }

    /**
     * 管理员更新用户信息方法
     * 此方法首先检查当前操作者是否已登录并具有管理员权限，以确保操作的安全性
     * 然后根据管理员提供的信息更新用户的数据，包括密码、签名、邮箱等
     *
     * @param updateUserByAdminDTO 包含了需要更新的用户信息的数据传输对象
     * @throws UnauthorizedException 如果操作者未登录，则抛出此异常
     * @throws GenericException 如果操作者权限不足或指定的用户不存在，则抛出此异常
     */
    @Override
    @CacheEvict(value = "userInfo", key = "#updateUserByAdminDTO.studentId")
    public void updateUserByAdmin(UpdateUserByAdminDTO updateUserByAdminDTO) {
        // 根据学生ID查询用户信息
        User user = userMapper.selectByStudentId(updateUserByAdminDTO.getStudentId());
        // 如果用户不存在，则抛出异常
        if (user == null){
            throw new GenericException("用户不存在");
        }
        // 如果提供了新密码，则更新密码
        if(updateUserByAdminDTO.getPassword()!=null){
            user.setPassword(BCrypt.hashpw(updateUserByAdminDTO.getPassword(), BCrypt.gensalt()));
        }
        // 注销用户
        StpUtil.logout(user.getStudentId());
        // 更新用户的签名
        user.setSign(updateUserByAdminDTO.getSign());
        // 更新用户的邮箱
        user.setEmail(updateUserByAdminDTO.getEmail());
        // 更新用户信息的修改时间
        user.setUpdateTime(LocalDateTime.now());
        userMapper.updateByStudentId(user);
    }

    /**
     * 修改用户密码
     *
     * @param changePasswordDTO 包含旧密码和新密码的DTO
     * 此方法首先检查用户是否已登录，然后验证旧密码是否正确，
     * 最后在确认新密码两次输入一致后，更新数据库中的密码
     */
    @Override
    public void changePassword(ChangePasswordDTO changePasswordDTO) {
        // 获取当前登录用户的用户名
        String username = StpUtil.getLoginIdAsString();
        // 根据用户名查询用户信息
        User user = selectByUsername(username);
        // 验证旧密码是否正确
        if (!BCrypt.checkpw(changePasswordDTO.getPassword(), user.getPassword())) {
            throw new GenericException("密码错误");
        }
        // 检查新密码和确认密码是否一致
        if (!changePasswordDTO.getNewPassword().equals(changePasswordDTO.getConfirmPassword())) {
            throw new GenericException("两次密码不一致");
        }
        // 加密新密码并更新到数据库
        user.setPassword(BCrypt.hashpw(changePasswordDTO.getNewPassword(), BCrypt.gensalt()));
        userMapper.updateByStudentId(user);
        // 注销用户
        StpUtil.logout();
    }

}